agentmux_srv\backend\wconfig/
types.rs

1// Copyright 2025-2026, AgentMux Corp.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Configuration type definitions: settings, themes, widgets, connections, bookmarks.
5
6use std::collections::HashMap;
7
8use serde::{Deserialize, Serialize};
9use serde_json::Value;
10
11use crate::backend::obj::MetaMapType;
12
13// ---- Serde helpers (used by skip_serializing_if attributes) ----
14
15pub(crate) fn is_false(v: &bool) -> bool {
16    !v
17}
18
19pub(crate) fn is_zero_f64(v: &f64) -> bool {
20    *v == 0.0
21}
22
23pub(crate) fn is_zero_f32(v: &f32) -> bool {
24    *v == 0.0
25}
26
27pub(crate) fn is_zero_i32(v: &i32) -> bool {
28    *v == 0
29}
30
31// ---- SettingsType ----
32
33/// Application settings. Matches Go's `wconfig.SettingsType` JSON tags.
34/// Fields use pointer-like `Option` for nullable booleans/numbers
35/// to distinguish "not set" from "false/0".
36#[derive(Debug, Clone, Default, Serialize, Deserialize)]
37pub struct SettingsType {
38    // -- App settings --
39    #[serde(rename = "app:*", default, skip_serializing_if = "is_false")]
40    pub app_clear: bool,
41
42    #[serde(rename = "app:globalhotkey", default, skip_serializing_if = "String::is_empty")]
43    pub app_global_hotkey: String,
44
45    #[serde(rename = "app:dismissarchitecturewarning", default, skip_serializing_if = "is_false")]
46    pub app_dismiss_architecture_warning: bool,
47
48    #[serde(rename = "app:defaultnewblock", default, skip_serializing_if = "String::is_empty")]
49    pub app_default_new_block: String,
50
51    #[serde(rename = "app:showoverlayblocknums", default, skip_serializing_if = "Option::is_none")]
52    pub app_show_overlay_block_nums: Option<bool>,
53
54    // -- Terminal settings --
55    #[serde(rename = "term:*", default, skip_serializing_if = "is_false")]
56    pub term_clear: bool,
57
58    #[serde(rename = "term:fontsize", default, skip_serializing_if = "is_zero_f64")]
59    pub term_font_size: f64,
60
61    #[serde(rename = "term:fontfamily", default, skip_serializing_if = "String::is_empty")]
62    pub term_font_family: String,
63
64    #[serde(rename = "term:theme", default, skip_serializing_if = "String::is_empty")]
65    pub term_theme: String,
66
67    #[serde(rename = "term:disablewebgl", default, skip_serializing_if = "is_false")]
68    pub term_disable_web_gl: bool,
69
70    #[serde(rename = "term:localshellpath", default, skip_serializing_if = "String::is_empty")]
71    pub term_local_shell_path: String,
72
73    #[serde(rename = "term:localshellopts", default, skip_serializing_if = "Vec::is_empty")]
74    pub term_local_shell_opts: Vec<String>,
75
76    #[serde(rename = "term:scrollback", default, skip_serializing_if = "Option::is_none")]
77    pub term_scrollback: Option<i64>,
78
79    #[serde(rename = "term:copyonselect", default, skip_serializing_if = "Option::is_none")]
80    pub term_copy_on_select: Option<bool>,
81
82    #[serde(rename = "term:transparency", default, skip_serializing_if = "Option::is_none")]
83    pub term_transparency: Option<f64>,
84
85    #[serde(rename = "term:allowbracketedpaste", default, skip_serializing_if = "Option::is_none")]
86    pub term_allow_bracketed_paste: Option<bool>,
87
88    #[serde(rename = "term:shiftenternewline", default, skip_serializing_if = "Option::is_none")]
89    pub term_shift_enter_newline: Option<bool>,
90
91    /// Maximum runtime in hours before the watchdog kills an agent pane.
92    /// 0 (default) disables the limit.
93    #[serde(rename = "term:agentmaxruntimehours", default, skip_serializing_if = "is_zero_f64")]
94    pub term_agent_max_runtime_hours: f64,
95
96    /// Minutes of PTY silence before the watchdog kills an idle agent pane.
97    /// 0 (default) disables the limit.
98    #[serde(rename = "term:agentidletimeoutmins", default, skip_serializing_if = "is_zero_f64")]
99    pub term_agent_idle_timeout_mins: f64,
100
101    // -- Command settings --
102    #[serde(rename = "cmd:env", default, skip_serializing_if = "HashMap::is_empty")]
103    pub cmd_env: HashMap<String, String>,
104
105    // -- Block header settings --
106    #[serde(rename = "blockheader:*", default, skip_serializing_if = "is_false")]
107    pub block_header_clear: bool,
108
109    #[serde(rename = "blockheader:showblockids", default, skip_serializing_if = "is_false")]
110    pub block_header_show_block_ids: bool,
111
112    // -- Preview settings --
113    #[serde(rename = "preview:showhiddenfiles", default, skip_serializing_if = "Option::is_none")]
114    pub preview_show_hidden_files: Option<bool>,
115
116    // -- Tab settings --
117    #[serde(rename = "tab:preset", default, skip_serializing_if = "String::is_empty")]
118    pub tab_preset: String,
119
120    // -- Widget settings --
121    #[serde(rename = "widget:*", default, skip_serializing_if = "is_false")]
122    pub widget_clear: bool,
123
124    #[serde(rename = "widget:showhelp", default, skip_serializing_if = "Option::is_none")]
125    pub widget_show_help: Option<bool>,
126
127    #[serde(rename = "widget:icononly", default, skip_serializing_if = "Option::is_none")]
128    pub widget_icon_only: Option<bool>,
129
130    // -- Window settings --
131    #[serde(rename = "window:*", default, skip_serializing_if = "is_false")]
132    pub window_clear: bool,
133
134    #[serde(rename = "window:transparent", default, skip_serializing_if = "is_false")]
135    pub window_transparent: bool,
136
137    #[serde(rename = "window:blur", default, skip_serializing_if = "is_false")]
138    pub window_blur: bool,
139
140    #[serde(rename = "window:opacity", default, skip_serializing_if = "Option::is_none")]
141    pub window_opacity: Option<f64>,
142
143    #[serde(rename = "window:bgcolor", default, skip_serializing_if = "String::is_empty")]
144    pub window_bg_color: String,
145
146    #[serde(rename = "window:reducedmotion", default, skip_serializing_if = "is_false")]
147    pub window_reduced_motion: bool,
148
149    #[serde(rename = "window:tilegapsize", default, skip_serializing_if = "Option::is_none")]
150    pub window_tile_gap_size: Option<i64>,
151
152    #[serde(rename = "window:showmenubar", default, skip_serializing_if = "is_false")]
153    pub window_show_menu_bar: bool,
154
155    #[serde(rename = "window:nativetitlebar", default, skip_serializing_if = "is_false")]
156    pub window_native_title_bar: bool,
157
158    #[serde(rename = "window:disablehardwareacceleration", default, skip_serializing_if = "is_false")]
159    pub window_disable_hardware_acceleration: bool,
160
161    #[serde(rename = "window:maxtabcachesize", default, skip_serializing_if = "is_zero_i32")]
162    pub window_max_tab_cache_size: i32,
163
164    #[serde(rename = "window:magnifiedblockopacity", default, skip_serializing_if = "Option::is_none")]
165    pub window_magnified_block_opacity: Option<f64>,
166
167    #[serde(rename = "window:magnifiedblocksize", default, skip_serializing_if = "Option::is_none")]
168    pub window_magnified_block_size: Option<f64>,
169
170    #[serde(rename = "window:magnifiedblockblurprimarypx", default, skip_serializing_if = "Option::is_none")]
171    pub window_magnified_block_blur_primary_px: Option<i64>,
172
173    #[serde(rename = "window:magnifiedblockblursecondarypx", default, skip_serializing_if = "Option::is_none")]
174    pub window_magnified_block_blur_secondary_px: Option<i64>,
175
176    #[serde(rename = "window:confirmclose", default, skip_serializing_if = "is_false")]
177    pub window_confirm_close: bool,
178
179    #[serde(rename = "window:savelastwindow", default, skip_serializing_if = "is_false")]
180    pub window_save_last_window: bool,
181
182    #[serde(rename = "window:dimensions", default, skip_serializing_if = "String::is_empty")]
183    pub window_dimensions: String,
184
185    #[serde(rename = "window:zoom", default, skip_serializing_if = "Option::is_none")]
186    pub window_zoom: Option<f64>,
187
188    // -- Telemetry settings --
189    #[serde(rename = "telemetry:*", default, skip_serializing_if = "is_false")]
190    pub telemetry_clear: bool,
191
192    #[serde(rename = "telemetry:enabled", default, skip_serializing_if = "is_false")]
193    pub telemetry_enabled: bool,
194
195    #[serde(rename = "telemetry:interval", default, skip_serializing_if = "is_zero_f64")]
196    pub telemetry_interval: f64,
197
198    #[serde(rename = "telemetry:numpoints", default, skip_serializing_if = "Option::is_none")]
199    pub telemetry_numpoints: Option<i64>,
200
201    // -- Connection settings --
202    #[serde(rename = "conn:*", default, skip_serializing_if = "is_false")]
203    pub conn_clear: bool,
204
205    // -- Network settings --
206    #[serde(rename = "network:lan_discovery", default, skip_serializing_if = "is_false")]
207    pub network_lan_discovery: bool,
208
209    // -- Voice settings --
210    //
211    // `voice:enabled` globally controls whether the per-pane microphone
212    // button is rendered. The default at the UX layer is "enabled" — the
213    // frontend treats `undefined`/absent as enabled, so we only need to
214    // model the explicit-disable case here. Users set this to `false` in
215    // `settings.json` to fully hide the buttons across all panes.
216    //
217    // Spec: docs/specs/SPEC_VOICE_INPUT_PER_PANE_2026_05_19.md §7 Phase 3.
218    #[serde(rename = "voice:enabled", default, skip_serializing_if = "Option::is_none")]
219    pub voice_enabled: Option<bool>,
220
221    // Speech-to-text engine: "whisper" (capture audio → server STT, the default
222    // and only engine that works in CEF) or "webspeech" (browser API — dev /
223    // real-Chromium only). Absent ⇒ whisper.
224    // Spec: docs/specs/SPEC_VOICE_STT_ENGINE_2026_06_20.md.
225    #[serde(rename = "voice:engine", default, skip_serializing_if = "Option::is_none")]
226    pub voice_engine: Option<String>,
227
228    // Groq API key for the hosted Whisper backend. Read server-side only; never
229    // sent to the renderer. The AGENTMUX_GROQ_API_KEY env var takes precedence.
230    #[serde(rename = "voice:groqApiKey", default, skip_serializing_if = "Option::is_none")]
231    pub voice_groq_api_key: Option<String>,
232
233    // Local whisper.cpp backend (voice:engine = "whisper-local"). Both are
234    // user-provided paths in v1 (auto-download is a follow-up). Env overrides:
235    // AGENTMUX_WHISPER_CLI / AGENTMUX_WHISPER_MODEL.
236    #[serde(rename = "voice:whisperCliPath", default, skip_serializing_if = "Option::is_none")]
237    pub voice_whisper_cli_path: Option<String>,
238    // GGML model name to auto-download on first use (default "base.en"). The
239    // explicit-path override below skips the download.
240    #[serde(rename = "voice:whisperModel", default, skip_serializing_if = "Option::is_none")]
241    pub voice_whisper_model: Option<String>,
242    #[serde(rename = "voice:whisperModelPath", default, skip_serializing_if = "Option::is_none")]
243    pub voice_whisper_model_path: Option<String>,
244
245    // -- Notification sounds --
246    //
247    // Spec: docs/specs/SPEC_SOUND_NOTIFICATIONS_2026_06_05.md §5.1.
248    //
249    // Master enable. Default at the UX layer is "on" — the frontend treats
250    // `undefined`/absent as enabled, so absence means sounds play. Users
251    // set this to `false` to fully silence the app.
252    #[serde(rename = "notify:sounds:enabled", default, skip_serializing_if = "Option::is_none")]
253    pub notify_sounds_enabled: Option<bool>,
254
255    // 0.0 to 1.0; default 0.6 if unset.
256    #[serde(rename = "notify:sounds:volume", default, skip_serializing_if = "Option::is_none")]
257    pub notify_sounds_volume: Option<f32>,
258
259    // Suppress a pane's sound when that pane is focused AND the window
260    // is in foreground. Default: true.
261    #[serde(rename = "notify:sounds:suppresswhenfocused", default, skip_serializing_if = "Option::is_none")]
262    pub notify_sounds_suppress_when_focused: Option<bool>,
263
264    // Per-event opt-out. Absence = on (default behavior).
265    #[serde(rename = "notify:sound:agent.turn.complete", default, skip_serializing_if = "Option::is_none")]
266    pub notify_sound_agent_turn_complete: Option<bool>,
267
268    #[serde(rename = "notify:sound:agent.turn.error", default, skip_serializing_if = "Option::is_none")]
269    pub notify_sound_agent_turn_error: Option<bool>,
270
271    #[serde(rename = "notify:sound:agent.turn.interrupted", default, skip_serializing_if = "Option::is_none")]
272    pub notify_sound_agent_turn_interrupted: Option<bool>,
273
274    #[serde(rename = "notify:sound:agent.message.accepted", default, skip_serializing_if = "Option::is_none")]
275    pub notify_sound_agent_message_accepted: Option<bool>,
276
277    #[serde(rename = "notify:sound:agent.message.rejected", default, skip_serializing_if = "Option::is_none")]
278    pub notify_sound_agent_message_rejected: Option<bool>,
279
280    // -- Tool-call tones (subliminal per-tool "voice") --
281    //
282    // Spec: docs/specs/SPEC_AGENT_TOOL_CALL_TONES_2026_06_05.md.
283    //
284    // Master enable. Default at the UX layer is "on" — absence is on
285    // by design (see spec §7).
286    #[serde(rename = "notify:tooltones:enabled", default, skip_serializing_if = "Option::is_none")]
287    pub notify_tooltones_enabled: Option<bool>,
288
289    // Independent gain (0.0–1.0; default 0.15). Layered below the
290    // shared master gain so master kill-switches both subsystems.
291    #[serde(rename = "notify:tooltones:volume", default, skip_serializing_if = "Option::is_none")]
292    pub notify_tooltones_volume: Option<f32>,
293
294    // Scope: "all" (default) plays for every pane in every window;
295    // "focused" plays only for the focused pane in the focused window.
296    // The intermediate "window" mode is reserved for v1.5.
297    #[serde(rename = "notify:tooltones:scope", default, skip_serializing_if = "Option::is_none")]
298    pub notify_tooltones_scope: Option<String>,
299
300    /// Catch-all for unknown/dynamic keys (e.g. `widget:hidden@defwidget@sysinfo`).
301    /// These pass through serde unchanged so the frontend can access them as flat settings keys.
302    #[serde(flatten, default, skip_serializing_if = "HashMap::is_empty")]
303    pub extra: HashMap<String, serde_json::Value>,
304}
305
306// ---- Supporting config types ----
307
308/// MIME type display configuration.
309#[derive(Debug, Clone, Default, Serialize, Deserialize)]
310pub struct MimeTypeConfigType {
311    #[serde(default, skip_serializing_if = "String::is_empty")]
312    pub icon: String,
313
314    #[serde(default, skip_serializing_if = "String::is_empty")]
315    pub color: String,
316}
317
318/// File definition for block widgets.
319#[derive(Debug, Clone, Default, Serialize, Deserialize)]
320pub struct FileDef {
321    #[serde(default, skip_serializing_if = "String::is_empty")]
322    pub content: String,
323
324    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
325    pub meta: HashMap<String, Value>,
326}
327
328/// Block definition for widgets.
329#[derive(Debug, Clone, Default, Serialize, Deserialize)]
330pub struct BlockDef {
331    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
332    pub files: HashMap<String, FileDef>,
333
334    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
335    pub meta: MetaMapType,
336}
337
338/// Widget configuration.
339#[derive(Debug, Clone, Default, Serialize, Deserialize)]
340pub struct WidgetConfigType {
341    #[serde(rename = "display:order", default, skip_serializing_if = "is_zero_f64")]
342    pub display_order: f64,
343
344    #[serde(rename = "display:hidden", default, skip_serializing_if = "is_false")]
345    pub display_hidden: bool,
346
347    /// Whether this widget is pinned to the action bar by default on new installs.
348    /// Once the user has a `widget:pinned` setting this field is ignored.
349    #[serde(rename = "display:pinned", default, skip_serializing_if = "is_false")]
350    pub display_pinned: bool,
351
352    #[serde(default, skip_serializing_if = "String::is_empty")]
353    pub icon: String,
354
355    #[serde(default, skip_serializing_if = "String::is_empty")]
356    pub color: String,
357
358    #[serde(default, skip_serializing_if = "String::is_empty")]
359    pub label: String,
360
361    #[serde(default, skip_serializing_if = "String::is_empty")]
362    pub description: String,
363
364    #[serde(default, skip_serializing_if = "is_false")]
365    pub magnified: bool,
366
367    #[serde(rename = "blockdef", default)]
368    pub block_def: BlockDef,
369}
370
371/// Terminal color theme.
372#[derive(Debug, Clone, Default, Serialize, Deserialize)]
373pub struct TermThemeType {
374    #[serde(rename = "display:name", default, skip_serializing_if = "String::is_empty")]
375    pub display_name: String,
376
377    #[serde(rename = "display:order", default, skip_serializing_if = "is_zero_f64")]
378    pub display_order: f64,
379
380    #[serde(default, skip_serializing_if = "String::is_empty")]
381    pub black: String,
382    #[serde(default, skip_serializing_if = "String::is_empty")]
383    pub red: String,
384    #[serde(default, skip_serializing_if = "String::is_empty")]
385    pub green: String,
386    #[serde(default, skip_serializing_if = "String::is_empty")]
387    pub yellow: String,
388    #[serde(default, skip_serializing_if = "String::is_empty")]
389    pub blue: String,
390    #[serde(default, skip_serializing_if = "String::is_empty")]
391    pub magenta: String,
392    #[serde(default, skip_serializing_if = "String::is_empty")]
393    pub cyan: String,
394    #[serde(default, skip_serializing_if = "String::is_empty")]
395    pub white: String,
396
397    #[serde(rename = "brightBlack", default, skip_serializing_if = "String::is_empty")]
398    pub bright_black: String,
399    #[serde(rename = "brightRed", default, skip_serializing_if = "String::is_empty")]
400    pub bright_red: String,
401    #[serde(rename = "brightGreen", default, skip_serializing_if = "String::is_empty")]
402    pub bright_green: String,
403    #[serde(rename = "brightYellow", default, skip_serializing_if = "String::is_empty")]
404    pub bright_yellow: String,
405    #[serde(rename = "brightBlue", default, skip_serializing_if = "String::is_empty")]
406    pub bright_blue: String,
407    #[serde(rename = "brightMagenta", default, skip_serializing_if = "String::is_empty")]
408    pub bright_magenta: String,
409    #[serde(rename = "brightCyan", default, skip_serializing_if = "String::is_empty")]
410    pub bright_cyan: String,
411    #[serde(rename = "brightWhite", default, skip_serializing_if = "String::is_empty")]
412    pub bright_white: String,
413
414    #[serde(default, skip_serializing_if = "String::is_empty")]
415    pub gray: String,
416    #[serde(rename = "cmdtext", default, skip_serializing_if = "String::is_empty")]
417    pub cmd_text: String,
418    #[serde(default, skip_serializing_if = "String::is_empty")]
419    pub foreground: String,
420    #[serde(rename = "selectionBackground", default, skip_serializing_if = "String::is_empty")]
421    pub selection_background: String,
422    #[serde(default, skip_serializing_if = "String::is_empty")]
423    pub background: String,
424    #[serde(default, skip_serializing_if = "String::is_empty")]
425    pub cursor: String,
426}
427
428/// Web bookmark.
429#[derive(Debug, Clone, Default, Serialize, Deserialize)]
430pub struct WebBookmark {
431    #[serde(default)]
432    pub url: String,
433
434    #[serde(default, skip_serializing_if = "String::is_empty")]
435    pub title: String,
436
437    #[serde(default, skip_serializing_if = "String::is_empty")]
438    pub icon: String,
439
440    #[serde(rename = "iconcolor", default, skip_serializing_if = "String::is_empty")]
441    pub icon_color: String,
442
443    #[serde(rename = "iconurl", default, skip_serializing_if = "String::is_empty")]
444    pub icon_url: String,
445
446    #[serde(rename = "display:order", default, skip_serializing_if = "is_zero_f64")]
447    pub display_order: f64,
448}
449
450/// Per-connection configuration keywords.
451/// Matches Go's `wconfig.ConnKeywords`.
452#[derive(Debug, Clone, Default, Serialize, Deserialize)]
453pub struct ConnKeywords {
454    // -- Connection settings --
455    #[serde(rename = "conn:shellpath", default, skip_serializing_if = "String::is_empty")]
456    pub conn_shell_path: String,
457
458    #[serde(rename = "conn:ignoresshconfig", default, skip_serializing_if = "Option::is_none")]
459    pub conn_ignore_ssh_config: Option<bool>,
460
461    // -- Display settings --
462    #[serde(rename = "display:hidden", default, skip_serializing_if = "Option::is_none")]
463    pub display_hidden: Option<bool>,
464
465    #[serde(rename = "display:order", default, skip_serializing_if = "is_zero_f32")]
466    pub display_order: f32,
467
468    // -- Terminal settings --
469    #[serde(rename = "term:*", default, skip_serializing_if = "is_false")]
470    pub term_clear: bool,
471
472    #[serde(rename = "term:fontsize", default, skip_serializing_if = "is_zero_f64")]
473    pub term_font_size: f64,
474
475    #[serde(rename = "term:fontfamily", default, skip_serializing_if = "String::is_empty")]
476    pub term_font_family: String,
477
478    #[serde(rename = "term:theme", default, skip_serializing_if = "String::is_empty")]
479    pub term_theme: String,
480
481    // -- Command settings --
482    #[serde(rename = "cmd:env", default, skip_serializing_if = "HashMap::is_empty")]
483    pub cmd_env: HashMap<String, String>,
484
485    #[serde(rename = "cmd:initscript", default, skip_serializing_if = "String::is_empty")]
486    pub cmd_init_script: String,
487
488    #[serde(rename = "cmd:initscript.sh", default, skip_serializing_if = "String::is_empty")]
489    pub cmd_init_script_sh: String,
490
491    #[serde(rename = "cmd:initscript.bash", default, skip_serializing_if = "String::is_empty")]
492    pub cmd_init_script_bash: String,
493
494    #[serde(rename = "cmd:initscript.zsh", default, skip_serializing_if = "String::is_empty")]
495    pub cmd_init_script_zsh: String,
496
497    #[serde(rename = "cmd:initscript.pwsh", default, skip_serializing_if = "String::is_empty")]
498    pub cmd_init_script_pwsh: String,
499
500    #[serde(rename = "cmd:initscript.fish", default, skip_serializing_if = "String::is_empty")]
501    pub cmd_init_script_fish: String,
502
503    // -- SSH settings --
504    #[serde(rename = "ssh:user", default, skip_serializing_if = "Option::is_none")]
505    pub ssh_user: Option<String>,
506
507    #[serde(rename = "ssh:hostname", default, skip_serializing_if = "Option::is_none")]
508    pub ssh_hostname: Option<String>,
509
510    #[serde(rename = "ssh:port", default, skip_serializing_if = "Option::is_none")]
511    pub ssh_port: Option<String>,
512
513    #[serde(rename = "ssh:identityfile", default, skip_serializing_if = "Vec::is_empty")]
514    pub ssh_identity_file: Vec<String>,
515
516    #[serde(rename = "ssh:batchmode", default, skip_serializing_if = "Option::is_none")]
517    pub ssh_batch_mode: Option<bool>,
518
519    #[serde(rename = "ssh:pubkeyauthentication", default, skip_serializing_if = "Option::is_none")]
520    pub ssh_pubkey_authentication: Option<bool>,
521
522    #[serde(rename = "ssh:passwordauthentication", default, skip_serializing_if = "Option::is_none")]
523    pub ssh_password_authentication: Option<bool>,
524
525    #[serde(rename = "ssh:kbdinteractiveauthentication", default, skip_serializing_if = "Option::is_none")]
526    pub ssh_kbd_interactive_authentication: Option<bool>,
527
528    #[serde(rename = "ssh:preferredauthentications", default, skip_serializing_if = "Vec::is_empty")]
529    pub ssh_preferred_authentications: Vec<String>,
530
531    #[serde(rename = "ssh:addkeystoagent", default, skip_serializing_if = "Option::is_none")]
532    pub ssh_add_keys_to_agent: Option<bool>,
533
534    #[serde(rename = "ssh:identityagent", default, skip_serializing_if = "Option::is_none")]
535    pub ssh_identity_agent: Option<String>,
536
537    #[serde(rename = "ssh:identitiesonly", default, skip_serializing_if = "Option::is_none")]
538    pub ssh_identities_only: Option<bool>,
539
540    #[serde(rename = "ssh:proxyjump", default, skip_serializing_if = "Vec::is_empty")]
541    pub ssh_proxy_jump: Vec<String>,
542
543    #[serde(rename = "ssh:userknownhostsfile", default, skip_serializing_if = "Vec::is_empty")]
544    pub ssh_user_known_hosts_file: Vec<String>,
545
546    #[serde(rename = "ssh:globalknownhostsfile", default, skip_serializing_if = "Vec::is_empty")]
547    pub ssh_global_known_hosts_file: Vec<String>,
548}
549
550/// Configuration error from parsing.
551#[derive(Debug, Clone, Default, Serialize, Deserialize)]
552pub struct ConfigError {
553    pub file: String,
554    pub err: String,
555}
556
557/// Webhook integration configuration.
558#[allow(dead_code)]
559#[derive(Debug, Clone, Default, Serialize, Deserialize)]
560pub struct WebhookConfigType {
561    #[serde(default)]
562    pub version: String,
563
564    #[serde(rename = "workspaceId", default)]
565    pub workspace_id: String,
566
567    #[serde(rename = "authToken", default)]
568    pub auth_token: String,
569
570    #[serde(rename = "cloudEndpoint", default)]
571    pub cloud_endpoint: String,
572
573    #[serde(default)]
574    pub enabled: bool,
575
576    #[serde(default)]
577    pub terminals: Vec<String>,
578}
579
580// ---- Full config container ----
581
582/// Complete application configuration.
583/// Matches Go's `wconfig.FullConfigType`.
584#[derive(Debug, Clone, Default, Serialize, Deserialize)]
585pub struct FullConfigType {
586    #[serde(default)]
587    pub settings: SettingsType,
588
589    #[serde(rename = "mimetypes", default)]
590    pub mime_types: HashMap<String, MimeTypeConfigType>,
591
592    #[serde(rename = "defaultwidgets", default)]
593    pub default_widgets: HashMap<String, WidgetConfigType>,
594
595    #[serde(default)]
596    pub widgets: HashMap<String, WidgetConfigType>,
597
598    #[serde(default)]
599    pub presets: HashMap<String, MetaMapType>,
600
601    #[serde(rename = "termthemes", default)]
602    pub term_themes: HashMap<String, TermThemeType>,
603
604    #[serde(default)]
605    pub connections: HashMap<String, ConnKeywords>,
606
607    #[serde(default)]
608    pub bookmarks: HashMap<String, WebBookmark>,
609
610    #[serde(rename = "configerrors", default, skip_serializing_if = "Vec::is_empty")]
611    pub config_errors: Vec<ConfigError>,
612}